今天開始 promise 的章節,會想讀這部分除了自己對它理解很差之外,覺得 promise 的世界有太多理所當然和習慣成自然,常常有類似這樣的討論。promise 有很多彈性和方案,但肯定不會所有事情總是習慣問題,希望寫完這段後可以更有自信的做出判斷。不過,在真的寫到 promise 前還要先寫一點 callbacks 作為前情提要。
JS 的同步與非同步本身就是很大的主題,這裡暫時就不特別延伸,主要的重點會放在如何處理非同步。基本的概念是:JS 的同步特性照著順序一次做一件事情,但有的時候這樣可能會有點問題 (事情花太多時間或想要延遲執行...等等)。因此 JS 中有非同步的行為,讓事情可以「現在開始,但晚點完成」,就可以正常做其他事並在事情完成時得到正確結果。
例如下方是一個用來載入檔案的 loadScript
函式,它做的事情是產生一個 script
標籤,並以傳入的參數作為 src 屬性值,接著把標籤放入文檔。然後瀏覽器就會非同步載入檔案 [註1],並在載入後開始執行:
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
// 呼叫 loadScript
loadScript('/my/script.js');
因為是非同步,代表 loadScript
執行完後檔案在背景載入,loadScript
底下的程式碼會繼續依序執行,不會等它完成。
也就是說,如果像下方,執行 loadScript
後想馬上調用檔案中的某個函式就會失敗:
loadScript('/my/script.js'); // 假設檔案中有 newFunction 這個函式
newFunction(); // X 沒有這個函式
於是這時候自然會有一個需求:我們想知道什麼時候可以調用檔案裡的東西。但是透過此時此刻的 loadScript
無法知道檔案何時載完,只知道它終究會載入執行而已。
所以稍微修改一下,讓 loadScript
再傳入一個 callback 參數,裡面包含檔案載完之後想要做的事情,接著監聽 script
在 onload 時執行 callback,這樣就能確保正確取到檔案裡的東西:
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
// 呼叫 loadScript
loadScript('/my/script.js', function() {
// 這個 callback 在 script 載完後執行,就可以使用 newFunction()
newFunction();
});
這樣的做法是以 callback 來處理非同步行為 (“callback-based” asynchronous programming),也就是非同步任務的函式會提供一個 callback 參數,用來執行務完成後想要做的事 [註2]。
換個角度或許可以形容成,非同步任務讓程式不會塞車卡死,但因為只有任務裡面才知道做完了沒,所以把要做的事一起打包送進去,才能正確進行。有一段時間這種 callback 作法是處理 JS 非同步的主要方式,後來不再是主流當然是因為它有一些缺點,接下來會寫到這個部分。
[註1] 當初盯著「非同步載入檔案」這句話很久,雖然好像蠻合理的,但我一直在想為什麼它一定就是非同步,後來 (竟然是在同一份文件) 看到這句 'Dynamic scripts behave as “async” by default.'。script 標籤的各種行為也是很值得做成筆記...
[註2] 「等特定事情發生,再呼叫 callback」的作法很像 JS 的事件處理,這段對於 JS 事件監聽和非同步有更多敘述。
另外也讀了一下這篇問答中對於非同步程式的敘述。